package com.conversationboard.model;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.map.LRUMap;
import org.joda.time.DateTime;
import org.joda.time.MutableDateTime;

import com.conversationboard.cache.BannedUserCache;
import com.conversationboard.cache.UserCache;
import com.conversationboard.config.Configuration;
import com.conversationboard.database.Database;
import com.conversationboard.database.Queries;
import com.conversationboard.dns.DNS;
import com.conversationboard.formatter.DateFormatter;
import com.conversationboard.model.exceptions.UserAlreadyExistsException;
import com.conversationboard.model.exceptions.UserDoesNotExistException;
import com.conversationboard.model.preferences.UserPreferences;
import com.conversationboard.model.userdata.UserData;
import com.conversationboard.network.UserLocation;
import com.conversationboard.site.Site;
import com.conversationboard.validate.InputChecker;
import com.twmacinta.util.MD5;

/** Class to represent a given user and any user-associated information.
 * 
 * UnreadMessageMap is a store that for each user holds the number of the last message in the thread the last
 * time this user looked at said thread. If this user falls out of the user cache, it is immediately
 * serialized and written to the database, so that when the user is next loaded from the database, it can be
 * repopulated. This ensures the the information is persistent, although clearly it will be lost if the server
 * is stop/started. This is a worthwhile tradeoff to ensure I/O savings and largely get the functionality we
 * want.
 * 
 * The comparable interface allows the user cache to be sorted by order of most recently accessed.
 * 
 * @author Keith Watson */

public class User implements Comparable<User> {

	private static final int UNREAD_MESSAGE_MAP_SIZE = 200;

	private int userId;
	private String loginId;
	private String displayName;
	private String md5Password;
	private Locale locale;
	private Role role;
	private boolean displaySubsetOfMessages = true;
	private BoardAdminList boardAdminList = new BoardAdminList();
	private String ipAddress = null;
	private UserPreferences preferences = new UserPreferences(this);
	private List<Integer> boardMembership = new LinkedList<Integer>();
	private Map unreadMessageMap = Collections.synchronizedMap(new LRUMap(UNREAD_MESSAGE_MAP_SIZE));
	private Date lastMessageTime;
	private int messageFloodCount;
	private Date creationDate;
	private Date lastLoggedIn;
	private Date lastAccessed;
	private Bookmarks bookmarks = new Bookmarks();
	private UserProfile userProfile = new UserProfile();
	private PrivilegeList privilegeList = new PrivilegeList();
	private String originalDisplayName;
	private Date yellowCardedOn = null;
	private String yellowCardReason = "";
	private int banCount;
	private int suggestBans;
	private int loginCount;
	private int postCount;
	private boolean permanent;
	private RecentlyUploadedFilesList recentlyUploadedFiles = new RecentlyUploadedFilesList();


	public boolean isPermanent() {
		return permanent;
	}


	public void setPermanent(boolean permanent) throws SQLException {

		Connection connection = Database.getConnection();

		try {

			PreparedStatement statement = connection.prepareStatement(Queries.SET_USER_PERMANENT);
			statement.setBoolean(1, permanent);
			statement.setString(2, this.getLoginId());

			statement.executeUpdate();

			statement.close();

			this.permanent = permanent;

		} finally {
			Database.close(connection);
		}

	}


	public int getPostCount() {
		return postCount;
	}


	public void setPostCount(int postCount) {
		this.postCount = postCount;
	}


	public void incrementPostCount() {
		postCount++;
	}


	public int getLoginCount() {
		return loginCount;
	}


	public void setLoginCount(int loginCount) {
		this.loginCount = loginCount;
	}


	public Date getLastLoggedIn() {
		return lastLoggedIn;
	}


	public void setLastLoggedIn(Date lastLoggedIn) {
		this.lastLoggedIn = lastLoggedIn;
	}


	public int getBanCount() {
		return banCount;
	}


	public void setBanCount(int banCount) {
		this.banCount = banCount;
	}


	public int getSuggestBans() {
		return suggestBans;
	}


	public void setSuggestBans(int suggestBans) {
		this.suggestBans = suggestBans;
	}


	public String getYellowCardReason() {
		return yellowCardReason;
	}


	public void setYellowCardReason(String yellowCardReason) {
		this.yellowCardReason = yellowCardReason;
	}


	public Date getYellowCardedOn() {
		return yellowCardedOn;
	}


	public void setYellowCardedOn(Date yellowCardedOn) {
		this.yellowCardedOn = yellowCardedOn;
	}


	public void setOriginalDisplayName(String originalDisplayName) {
		this.originalDisplayName = originalDisplayName;
	}


	public boolean isLoggedIn() {
		if (this.getRole() != Role.Anonymous) {
			return true;
		} else {
			return false;
		}
	}


	public int getUserId() {
		return userId;
	}


	public void setUserId(int userId) {
		this.userId = userId;
	}


	/** Users are not allowed to post for a period after registration, as configured in the site
	 * Configuration.getInstance(). This function checks if you have gone past this period.
	 * 
	 * @return whether or not you can post */

	public boolean isAbleToPost(int boardId) throws SQLException {

		if (this.isBoardAdmin(boardId)) {
			return true;
		}

		DateTime creation = new DateTime(this.creationDate);
		Date now = new Date();

		Board board = Boards.getBoard(boardId);
		int days = board.getSandboxDays();

		if (creation.plusDays(days).isAfter(now.getTime())) {
			return false;
		} else {
			return true;
		}
	}


	public int compareTo(User user) {
		int result = user.lastAccessed.compareTo(this.lastAccessed);
		return result;
	}


	public String getHostName() {

		if ((this.ipAddress == null) || (this.ipAddress.equals("0.0.0.0")) || (this.ipAddress.equals("0:0:0:0:0:0:0:1%0"))) {
			return "Could not determine host name";
		} else {
			try {
				return DNS.dnsLoopup(this.ipAddress);
			} catch (IOException e) {
				return "Could not determine host name";
			}
		}
	}


	public void setLastAccessed(Date lastAccessed) {
		this.lastAccessed = lastAccessed;
	}


	public Date getLastAccessed() {
		return lastAccessed;
	}


	public String getLastAccessedString() {

		if (this.lastAccessed == null) {
			return "";
		}

		String message = DateFormatter.prettyPrintTime(this.lastAccessed, this, true, true);

		if (message.equals("0 seconds")) {
			message = "Now";
		}

		return message;

	}


	public String getMd5Password() {
		return md5Password;
	}


	public void setMd5Password(String md5Password) {
		this.md5Password = md5Password;
	}


	public PrivilegeList getPrivilegeList() {
		return privilegeList;
	}


	public void setPrivilegeList(PrivilegeList privilegeList) {
		this.privilegeList = privilegeList;
	}


	public Bookmarks getBookmarks() {
		return bookmarks;
	}


	public int getMessageFloodCount() {
		return messageFloodCount;
	}


	public void incrementMessageFloodCount() {
		this.messageFloodCount++;
	}


	public void setMessageFloodCount(int messageFloodCount) {
		this.messageFloodCount = messageFloodCount;
	}


	public Date getLastMessageTime() {
		return lastMessageTime;
	}


	public void setLastMessageTime(Date lastMessageTime) {
		this.lastMessageTime = lastMessageTime;
	}


	public void setPreferences(UserPreferences preferences) {
		this.preferences = preferences;
	}


	public Locale getLocale() {

		if (this.locale == null) {
			return Locale.UK;
		} else {
			return this.locale;
		}

	}


	/* Copy Constructor. Doesn't copy user preferences... */

	public static User copyUser(User user) {

		User newUser = new User();
		newUser.setDisplayName(user.getDisplayName());
		newUser.setIpAddress(user.getIpAddress());
		newUser.setLocale(user.getLocale());
		newUser.setLoginId(user.getLoginId());
		newUser.setRole(user.getRole());

		for (int privateBoardId : user.getBoardMembership()) {
			newUser.getBoardMembership().add(privateBoardId);
		}

		return newUser;
	}


	private String getKey(int boardId, int threadId) {
		return boardId + "#" + threadId;
	}


	public void setMostRecentMessageId(int boardId, int threadId, int mostRecentMessageId) {

		String key = this.getKey(boardId, threadId);

		this.unreadMessageMap.put(key, mostRecentMessageId);
	}


	public int getMostRecentMessageId(int boardId, int threadId) {

		String key = this.getKey(boardId, threadId);

		Integer id = (Integer) this.unreadMessageMap.get(key);

		if (id == null) {
			return 0;
		} else {
			return id;
		}

	}


	public void setLocale(Locale locale) {
		this.locale = locale;
	}


	public List<Integer> getBoardMembership() {
		return this.boardMembership;
	}


	public boolean isMemberOfBoard(int boardId) {
		return this.boardMembership.contains(boardId);
	}


	public void allowAccessToBoard(int boardId) throws UserDoesNotExistException, SQLException {

		Connection connection = Database.getConnection();

		try {

			/* First check if they're already authorised */

			if (this.isMemberOfBoard(boardId)) {
				return;
			}

			/* If they're not, update the database */

			PreparedStatement statement = connection.prepareStatement(Queries.GRANT_BOARD_MEMBERSHIP);
			statement.setString(1, this.getLoginId());
			statement.setInt(2, boardId);

			int numberOfUpdates = statement.executeUpdate();

			statement.close();

			if (numberOfUpdates == 0) {
				throw new UserDoesNotExistException();
			}

			/* Then cached version */

			this.boardMembership.add(boardId);

		} finally {
			Database.close(connection);
		}

	}


	public UserPreferences getPreferences() {
		return preferences;
	}


	public boolean hasIpAddress() {
		if (ipAddress == null) {
			return false;
		} else {
			return true;
		}

	}


	public String getIpAddress() {
		return ipAddress;
	}


	public void setIpAddress(String ipAddress) {
		this.ipAddress = ipAddress;
	}


	public Role getRole() {
		return role;
	}


	public void setRole(Role role) {
		this.role = role;
	}


	public boolean isDisplaySubsetOfMessages() {
		return displaySubsetOfMessages;
	}


	public boolean displaySubsetOfMessages() {
		return displaySubsetOfMessages;
	}


	public void setDisplaySubsetOfMessages(boolean displaySubsetOfMessages) {
		this.displaySubsetOfMessages = displaySubsetOfMessages;
	}


	public String getDisplayName() {
		return displayName;
	}


	public void setDisplayName(String displayName) {
		this.displayName = displayName;
	}


	public String getLoginId() {
		return loginId;
	}


	public void setLoginId(String loginId) {
		this.loginId = loginId.toLowerCase();
	}


	public User() {
	}


	public void updateDisplayNames(String displayName, String originalDisplayName) throws SQLException {

		Connection connection = Database.getConnection();

		try {

			PreparedStatement statement = connection.prepareStatement(Queries.UPDATE_DISPLAY_NAMES);
			statement.setString(1, displayName);
			statement.setString(2, originalDisplayName);
			statement.setInt(3, this.getUserId());

			statement.executeUpdate();

			this.removeFromCache();

		} finally {
			Database.close(connection);
		}

	}


	public static User getAnonymousUser() {

		User user = new User();

		user = new User();
		user.setRole(Role.Anonymous);
		user.setDisplayName("Anonymous");
		user.setDisplaySubsetOfMessages(true);
		user.setLoginId("Anonymous");
		user.setLocale(Locale.UK);
		user.boardAdminList = new BoardAdminList();

		return user;
	}


	public static User getSystemUser() {

		User systemUser = User.getAnonymousUser();

		systemUser.setDisplayName("System");
		systemUser.setLoginId("donotreply@donotreply.com");
		systemUser.setRole(Role.SiteAdmin);

		return systemUser;
	}


	public static boolean isUserRegistered(String displayName) throws SQLException {

		Connection connection = Database.getConnection();

		try {

			boolean registered = false;

			PreparedStatement statement = connection.prepareStatement(Queries.GET_DISPLAYNAME_QUERY);

			statement.setString(1, displayName);
			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			/* Only expect one result here */

			while (resultSet.next()) {
				String login = resultSet.getString("displayname");
				if (login.equals(displayName)) {
					registered = true;
				}
			}

			statement.close();

			return registered;

		} finally {
			Database.close(connection);
		}
	}


	/** Typically used only from error logging as it can provide additional diagnostics. It's necessary to
	 * prevent the possibility of an exception being thrown from the error servlet setting up a recursion.
	 * 
	 * @param loginId
	 * @return */

	public static User getOnlyIfInCache(String loginId) {

		if (loginId == null || loginId.equals("Anonymous")) {
			return getAnonymousUser();
		}

		/* Is the user in the cache? */

		User user = UserCache.getInstance().getUser(loginId);

		if (user != null) {
			return user;
		} else {
			return getAnonymousUser();
		}

	}


	public static User get(Principal principal) throws SQLException {

		if (principal == null) {
			return User.getAnonymousUser();
		} else {
			return User.get(principal.getName());
		}
	}


	public static User get(String loginId) throws SQLException {

		if (loginId == null || loginId.equalsIgnoreCase("Anonymous")) {
			return getAnonymousUser();
		}

		/* Tomcat can pass mixed case login IDs, so set to lower case */

		loginId = loginId.toLowerCase();

		/* Is the user in the cache? */

		User user = UserCache.getInstance().getUser(loginId);

		if (user != null) {
			return user;
		}

		/* Look in the database */

		Connection connection = Database.getConnection();

		try {
			PreparedStatement statement = connection.prepareStatement(Queries.GET_USER_QUERY);

			statement.setString(1, loginId);
			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			/* Only expect one result here */

			while (resultSet.next()) {

				user = new User();

				String role = resultSet.getString("role");
				Timestamp creationDate = resultSet.getTimestamp("creationdate");

				if (role.equals("user")) {
					user.setRole(Role.User);
				} else if (role.equals("boardadmin")) {
					user.setRole(Role.BoardAdmin);
				} else if (role.equals("siteadmin")) {
					user.setRole(Role.SiteAdmin);
				}

				int userId = resultSet.getInt("id");
				String preferencesXml = resultSet.getString("preferences");
				String userProfileXml = resultSet.getString("profile");
				String privilegeListXml = resultSet.getString("privilegelist");
				String originalDisplayName = resultSet.getString("originaldisplayname");

				if ((preferencesXml != null) && (preferencesXml.length() > 0)) {
					UserPreferences preferences = UserPreferences.load(user, preferencesXml.trim());
					user.setPreferences(preferences);
				}

				if ((userProfileXml != null) && (userProfileXml.length() > 0)) {
					UserProfile userProfile = UserProfile.deserialise(userProfileXml);
					user.userProfile = userProfile;
				}

				if ((privilegeListXml != null) && (privilegeListXml.length() > 0)) {
					PrivilegeList privilegeList = PrivilegeList.deserialise(privilegeListXml);
					user.privilegeList = privilegeList;
				}

				user.setUserId(userId);
				user.setOriginalDisplayName(originalDisplayName);
				user.setDisplayName(resultSet.getString("displayname"));
				user.setMd5Password(resultSet.getString("password"));
				user.setDisplaySubsetOfMessages(true);
				user.setLoginId(loginId);
				user.setLastLoggedIn(resultSet.getDate("lastloggedin"));
				user.loginCount = resultSet.getInt("logincount");
				user.postCount = resultSet.getInt("postcount");
				user.banCount = resultSet.getInt("bancount");
				user.permanent = resultSet.getBoolean("permanent");

				String ipAddress = resultSet.getString("ipaddress");
				user.setIpAddress(ipAddress);

				user.creationDate = creationDate;
				user.boardAdminList = new BoardAdminList(resultSet.getString("boards"));
				user.setYellowCardedOn(resultSet.getTimestamp("yellowcard"));
				user.setYellowCardReason(resultSet.getString("reason"));

				String threadPositions = resultSet.getString("threadpositions");
				user.loadUnreadMessageMap(threadPositions);

				user.lastAccessed = user.getLastLoggedIn();
			}

			statement.close();

			/* Keith: could consider returning a user with a name like "[Deleted User]" instead of null here. */

			if (user == null) {
				return null;
			}

			/* Get the private board membership */

			PreparedStatement statement2 = connection.prepareStatement(Queries.GET_BOARD_MEMBERSHIP);

			statement2.setString(1, loginId);
			statement2.execute();

			ResultSet resultSet2 = statement2.getResultSet();

			while (resultSet2.next()) {
				int boardId = resultSet2.getInt("boardid");
				user.boardMembership.add(boardId);
			}

			statement2.close();

			/* Count suggest bans and set the ban count */

			PreparedStatement statement3 = connection.prepareStatement(Queries.COUNT_SUGGEST_BANS);

			statement3.setString(1, loginId);
			statement3.execute();

			ResultSet resultSet3 = statement3.getResultSet();

			while (resultSet3.next()) {
				user.setSuggestBans(resultSet3.getInt("total"));
			}

			statement3.close();

			/* Get this user's five most recently uploaded files, if we support file uploads */

			if (Configuration.getInstance().isSupportFileUploads()) {

				List<UploadedFile> files = FileUpload.getUploadedFiles(user, 0, 5);

				user.getRecentlyUploadedFiles().addAll(files);

			}

			/* Load your bookmarks */

			user.setBookmarks(Bookmarks.load(user));

			/* Add the user to the cache */
			UserCache.getInstance().addUser(user);

		} finally {
			Database.close(connection);
		}
		return user;
	}


	public static void checkForExistence(String loginId, String displayName) throws SQLException, UserAlreadyExistsException {

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement checkIfExists = connection.prepareStatement(Queries.CHECK_IF_USER_EXISTS_QUERY);

			checkIfExists.setString(1, loginId);
			checkIfExists.setString(2, displayName);
			checkIfExists.execute();

			ResultSet resultSet = checkIfExists.getResultSet();

			boolean loginIdFound = false;
			boolean displayNameFound = false;

			while (resultSet.next()) {

				String databaseLoginId = resultSet.getString("loginid");
				String databaseDisplayName = resultSet.getString("displayname");

				if (databaseLoginId.equals(loginId)) {
					loginIdFound = true;
				}

				if (databaseDisplayName.equals(displayName)) {
					displayNameFound = true;
				}
			}

			if (loginIdFound && displayNameFound) {
				throw new UserAlreadyExistsException(loginId, displayName);
			} else if (loginIdFound) {
				throw new UserAlreadyExistsException(loginId, (String) null);
			} else if (displayNameFound) {
				throw new UserAlreadyExistsException((String) null, displayName);
			}

			resultSet.close();

		} finally {
			Database.close(connection);
		}
	}


	/** User the above method to check for existence of the user before calling this method.
	 * 
	 * @param name
	 * @param loginId
	 * @param displayName
	 * @param password
	 * @param timeZone
	 * @throws SQLException
	 * @throws UserAlreadyExistsException */

	public static void add(String name, String loginId, String displayName, String password, String timeZoneString, String ipAddress) throws SQLException, UserAlreadyExistsException {

		Connection connection = null;

		try {

			connection = Database.getConnection();
			connection.setAutoCommit(false);

			PreparedStatement insertUser = connection.prepareStatement(Queries.INSERT_USER_QUERY);

			Date today = new Date();

			TimeZone timeZone = TimeZone.getTimeZone(timeZoneString);

			insertUser.setString(1, loginId);
			insertUser.setString(2, name);
			insertUser.setTimestamp(3, new Timestamp(today.getTime()));
			insertUser.setString(4, password);
			insertUser.setString(5, timeZone.getID());
			insertUser.setString(6, Configuration.getInstance().getDefaultStyleSheet());
			insertUser.setString(7, ipAddress);

			insertUser.executeUpdate();

			PreparedStatement insertRole = connection.prepareStatement(Queries.INSERT_USER_ROLE_QUERY);

			insertRole.setString(1, loginId);
			insertRole.setString(2, "user");

			insertRole.executeUpdate();

			/* Create a user object and add the user to the cache, so we don't need to go back to the database
			 * again (there's no code to do this anyway; all users get read on first case where they're
			 * needed). */

			User user = new User();
			user.setRole(Role.User);
			user.setDisplayName(name);
			user.setLoginId(loginId);
			user.setCreationDate(today);
			user.setDisplaySubsetOfMessages(true);
			user.setLastAccessed(new Date());
			user.setIpAddress(ipAddress);

			/* Create and store the preferences in the new way */

			UserPreferences preferences = new UserPreferences(user);
			preferences.setTimeZone(timeZone);
			preferences.store(connection);

			/* Create and store the user's privileges (with default values) */

			PrivilegeList privilegeList = new PrivilegeList();
			privilegeList.store(loginId, connection);

			UserCache.getInstance().addUser(user);

			connection.commit();

			Site.incrementUserCount();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}
	}


	/** Makes this user a site administrator
	 * 
	 * @return true, if it worked; false, if the user does not exist
	 * @throws SQLException */

	public boolean makeSiteAdmin() throws SQLException {

		if (this.getRole() == Role.Anonymous) {
			return false;
		}

		Connection connection = null;

		try {

			connection = Database.getConnection();
			PreparedStatement statement = connection.prepareStatement(Queries.UPDATE_USER_SITE_ADMIN);

			statement.setString(1, this.getLoginId());
			int usersUpdated = statement.executeUpdate();

			if (usersUpdated == 1) {
				this.setRole(Role.SiteAdmin);
				return true;
			} else {
				return false;
			}

		} finally {
			Database.close(connection);
		}
	}


	public boolean isSiteAdmin() {

		if (this.getRole() == Role.SiteAdmin) {
			return true;
		} else {
			return false;
		}

	}


	public boolean isAdmin() {

		if (this.getRole() == Role.SiteAdmin || this.getRole() == Role.BoardAdmin) {
			return true;
		} else {
			return false;
		}
	}


	/** A user is allowed to administer this board if: (a) they are a site admin, or (b) they are an
	 * administrator of this board
	 * 
	 * @param boardId
	 *            the board to check if this user administrates
	 * @return whether or not you have administrative privileges */

	public boolean isBoardAdmin(int boardId) {

		/* Board ID of zero sometimes signifies that the action does not apply to a board, but to the whole
		 * site, so only OK site admins in this instance. */

		if (boardId == 0) {
			if (this.getRole() == Role.SiteAdmin) {
				return true;
			} else {
				return false;
			}
		}

		if ((this.getRole() == Role.SiteAdmin) || (boardAdminList.isAdmin(boardId) && this.getRole() == Role.BoardAdmin)) {
			return true;
		} else {
			return false;
		}

	}


	public boolean makeBoardAdmin(int boardId) throws SQLException {

		if (this.role != Role.SiteAdmin) {
			this.boardAdminList.makeAdmin(boardId);
			this.role = Role.BoardAdmin;
			this.boardAdminList.persist(this.getLoginId());
			return true;
		} else {
			return false;
		}

	}


	/** Sending the board, thread and message may seem over the top, but it's a clustered index on the table,
	 * so will speed up access.
	 * 
	 * @param boardId
	 *            the board
	 * @param threadId
	 *            the thread
	 * @param messageId
	 *            the message
	 * @return the user ID (email address) of the message sender
	 * @throws SQLException */

	public static String getLoginIdFromMessageId(int boardId, int threadId, int messageId) throws SQLException {

		Connection connection = Database.getConnection();
		String userId = "";

		try {

			PreparedStatement statement = connection.prepareStatement(Queries.GET_USERID_FROM_MESSAGE);

			statement.setInt(1, boardId);
			statement.setInt(2, threadId);
			statement.setInt(3, messageId);

			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			while (resultSet.next()) {
				userId = resultSet.getString("userid").toLowerCase();
			}

			resultSet.close();

		} finally {
			Database.close(connection);
		}

		return userId;

	}


	public void changePassword(String newPassword) throws SQLException {

		Connection connection = null;

		try {

			MD5 md5 = new MD5();
			md5.Update(newPassword, null);
			String hash = md5.asHex();

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.CHANGE_PASSWORD);
			statement.setString(1, hash);
			statement.setString(2, this.getLoginId());

			statement.executeUpdate();

		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		} finally {
			Database.close(connection);
		}
	}


	public static void remove(String loginId) throws SQLException {

		/* Firstly the database */

		Connection connection = null;

		try {

			connection = Database.getConnection();
			connection.setAutoCommit(false);

			PreparedStatement statement = connection.prepareStatement(Queries.DELETE_USER);
			statement.setString(1, loginId);
			int deleted = statement.executeUpdate();

			if (deleted == 0) {
				throw new SQLException("Failed to find user to delete");
			}

			statement = connection.prepareStatement(Queries.DELETE_USERROLE);
			statement.setString(1, loginId);
			statement.executeUpdate();

			statement.close();

			UserCache.getInstance().removeUser(loginId);

			connection.commit();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}

	}


	public void removeFromCache() {

		UserCache.getInstance().removeUser(this.getLoginId());

	}


	public static List<User> getUsersWithSameIpAddress(String ipAddress) throws SQLException {
		return UserCache.getInstance().getUsersWithSameIpAddress(ipAddress);
	}


	public static List<UserData> getUsersWithSamePassword(User user) throws SQLException {

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_PASSWORD_FOR_USER);
			statement.setString(1, user.getLoginId());
			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			String password = "";

			while (resultSet.next()) {
				password = resultSet.getString("password");
			}

			statement.close();

			statement = connection.prepareStatement(Queries.GET_USERS_WITH_SAME_PASSWORD);
			statement.setString(1, password);
			statement.execute();

			resultSet = statement.getResultSet();

			List<UserData> users = new LinkedList<UserData>();

			while (resultSet.next()) {

				UserData userData = new UserData();
				userData.setLoginId(resultSet.getString("loginid"));
				userData.setDisplayName(resultSet.getString("displayname"));
				userData.setOriginalDisplayName(resultSet.getString("originaldisplayname"));
				userData.setCreationDate(resultSet.getDate("creationdate"));
				userData.setLastLoggedIn(resultSet.getDate("lastloggedin"));
				userData.setPostCount(resultSet.getInt("postcount"));
				userData.setLoginCount(resultSet.getInt("logincount"));

				users.add(userData);
			}

			statement.close();

			return users;

		} finally {
			Database.close(connection);
		}
	}


	/** Send null into reason, if you don't yet have a reason.
	 * 
	 * @param admin
	 * @param reason
	 * @param temporary
	 * @throws SQLException */

	public void ban(User admin, String reason, boolean temporary) throws SQLException {
		this.ban(admin, reason, temporary, 0);
	}


	public void ban(User admin, String reason, boolean temporary, int boardId) throws SQLException {

		BannedUserCache.ban(admin, this.getFullDisplayName(), this.getLoginId(), !temporary, false, boardId, this.getBanCount(), reason, true);

		if (this.getIpAddress() != null) {
			BannedUserCache.ban(admin, this.getFullDisplayName(), this.getIpAddress(), !temporary, false, boardId, this.getBanCount(), reason, false);
		}

		/* Update ban count for this user. Really ought to be part of a wider transaction, but unlikely to
		 * fail. */

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.UPDATE_BAN_COUNT);
			statement.setInt(1, this.getUserId());
			statement.executeUpdate();

			statement.close();

			this.setBanCount(this.getBanCount() + 1);

		} finally {
			Database.close(connection);
		}
	}


	public void setBoardMembership(List<Integer> boardMembership) {
		this.boardMembership = boardMembership;
	}


	/* No longer need this code, as the new authenticator ensures users can be kept logged in */

	public void keepSessionAlive(HttpServletRequest request, HttpServletResponse response) {

		request.getSession().setMaxInactiveInterval(14400);

		Cookie[] cookies = request.getCookies();
		Cookie sessionCookie = null;

		if (cookies != null) {
			for (Cookie cookie : cookies) {
				if (cookie.getName().equals("JSESSIONID")) {
					sessionCookie = cookie;
				}
			}
		}

		if (sessionCookie != null) {
			sessionCookie.setMaxAge(86400);
			sessionCookie.setPath(Configuration.getInstance().getRoot());
			response.addCookie(sessionCookie);
		}
	}


	public BoardAdminList getBoardAdminList() {
		return boardAdminList;
	}


	public void setBoardAdminList(BoardAdminList boardAdminList) {
		this.boardAdminList = boardAdminList;
	}


	public void setCreationDate(Date creationDate) {
		this.creationDate = creationDate;
	}


	public Date getCreationDate() {
		return creationDate;
	}


	public String getCreationDateString() {

		if (this.creationDate == null) {
			return "";
		}

		String message = DateFormatter.prettyPrintTime(this.creationDate, this, true, true);

		if (message.equals("0 seconds")) {
			message = "Now";
		}

		return message;
	}


	public boolean changeDisplayName(String newDisplayName) throws SQLException {

		Connection connection = null;

		try {

			connection = Database.getConnection();
			connection.setAutoCommit(false);

			String loginId = this.getLoginId();
			newDisplayName = newDisplayName.trim();

			if ((InputChecker.containsIllegalCharacter(newDisplayName) || InputChecker.containsBiDirectionalCodes(newDisplayName) || newDisplayName.contains("%"))) {
				return false;
			}

			/* If you've set it to be nothing, then clear it back to your original user name */

			if (newDisplayName.length() == 0) {
				newDisplayName = this.getOriginalDisplayName();
			}

			PreparedStatement statement = connection.prepareStatement(Queries.SET_USER_DISPLAY_NAME);
			statement.setString(1, newDisplayName);
			statement.setString(2, loginId);

			statement.executeUpdate();

			statement.close();

			if ((this.originalDisplayName == null) || (this.originalDisplayName.trim().equals(""))) {
				this.setOriginalDisplayName(this.getDisplayName());
				this.storeOriginalDisplayName(connection);
			}

			connection.commit();

			this.setDisplayName(newDisplayName);

			return true;

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}
	}


	/** Administrative function to allow an admin to override a user's original display name.
	 * 
	 * @param displayName */

	public void updateOriginalDisplayName(String displayName) throws SQLException {

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.STORE_OLD_USER_NAME);
			statement.setString(1, displayName);
			statement.setString(2, this.getLoginId());
			statement.executeUpdate();

		} finally {
			Database.close(connection);
		}
	}


	private void storeOriginalDisplayName(Connection connection) throws SQLException {

		PreparedStatement statement = connection.prepareStatement(Queries.STORE_OLD_USER_NAME);
		statement.setString(1, this.getOriginalDisplayName());
		statement.setString(2, this.getLoginId());
		statement.executeUpdate();
	}


	public void changeOriginalDisplayname(String newDisplayName) throws SQLException {

		Connection connection = null;

		try {

			connection = Database.getConnection();
			connection.setAutoCommit(false);

			if (this.hasChangedUserName()) {
				this.setOriginalDisplayName(newDisplayName);
				this.storeOriginalDisplayName(connection);
			} else {
				PreparedStatement statement = connection.prepareStatement(Queries.SET_USER_DISPLAY_NAME);
				statement.setString(1, newDisplayName);
				statement.executeUpdate();
			}

			connection.commit();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}
	}


	/** This should only be called by the user cache when the user falls out of the cache. It's too heavyweight
	 * an operation to be called all of the time.
	 * 
	 * @throws SQLException */

	public void storeUnreadMessageMap() throws SQLException, IOException {

		/* Serialise the message map - proprietary format to make it small */

		StringBuilder buffer = new StringBuilder();
		int counter = 0;

		for (Iterator i = this.unreadMessageMap.entrySet().iterator(); i.hasNext();) {

			Map.Entry<String, Integer> entry = (Map.Entry<String, Integer>) i.next();

			/* This get is modifying the collection - can't have that */
			String key = entry.getKey();
			int value = entry.getValue();

			buffer.append(key + ":" + value);

			if (++counter != this.unreadMessageMap.size()) {
				buffer.append("*");
			}
		}

		/* Store it in the database */

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.USER_STORE_UNREAD_MESSAGE_MAP);
			statement.setString(1, buffer.toString());
			statement.setString(2, this.getLoginId());

			statement.executeUpdate();

		} finally {
			Database.close(connection);
		}

	}


	/** Load the proprietary text format from the database back into the map.
	 * 
	 * @param unreadMessageMap */

	private void loadUnreadMessageMap(String unreadMessageMap) {

		if ((unreadMessageMap == null) || unreadMessageMap.trim().length() == 0) {
			return;
		}

		this.unreadMessageMap = Collections.synchronizedMap(new LRUMap(UNREAD_MESSAGE_MAP_SIZE));

		String[] items = unreadMessageMap.split("\\*");

		for (String item : items) {

			String[] unreadItem = item.split("\\:");

			String boardAndThread = unreadItem[0];
			int messageId = Integer.parseInt(unreadItem[1]);

			this.unreadMessageMap.put(boardAndThread, messageId);
		}

	}


	public void storeUserProfile() throws SQLException {

		Connection connection = null;

		try {
			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.USER_STORE_PROFILE);
			statement.setString(1, this.userProfile.serialise());
			statement.setString(2, this.getLoginId());

			statement.executeUpdate();

			statement.close();

		} finally {
			Database.close(connection);
		}

	}


	public void storePrivileges() throws SQLException {
		this.storePrivilegeList(null);
	}


	private void storePrivilegeList(Connection connection) throws SQLException {

		boolean widerTransaction = (connection != null);

		try {
			if (connection == null) {
				connection = Database.getConnection();
			}

			this.privilegeList.store(this.getLoginId(), connection);

		} finally {
			if (!widerTransaction) {
				Database.close(connection);
			}
		}

	}


	public void setBookmarks(Bookmarks bookmarks) {
		this.bookmarks = bookmarks;
	}


	public UserProfile getUserProfile() {
		return userProfile;
	}


	public void setUserProfile(UserProfile userProfile) {
		this.userProfile = userProfile;
	}


	/** Returns true if the user is authorised to add new users to a private, or invite only board.
	 * 
	 * @param board
	 * @return */

	public boolean canAddUsersToBoard(Board board) {

		if (board.isPrivateBoard() || board.isInviteOnly()) {

			if (!board.isAdminsAddUsers()) {
				return true;
			} else if (this.isBoardAdmin(board.getBoardId())) {
				return true;
			}
		}

		return false;
	}


	public List<Board> getSubscriptions() throws SQLException {

		Connection connection = null;
		List<Board> subscriptions = new LinkedList<Board>();

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_USER_SUBSCRIPTIONS);
			statement.setString(1, this.getLoginId());
			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			while (resultSet.next()) {
				int boardId = resultSet.getInt("boardId");
				Board board = Boards.getBoard(boardId);
				subscriptions.add(board);
			}

			statement.close();

			return subscriptions;

		} finally {
			Database.close(connection);
		}

	}


	public void subscribe(int boardId) throws SQLException {

		Connection connection = null;

		/* Ensure you are authorised to subcribe to this board */

		if (!this.canRead(boardId)) {
			return;

		}

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.IS_USER_SUBSCRIBED_TO_BOARD);
			statement.setString(1, this.getLoginId());
			statement.setInt(2, boardId);

			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			int count = 0;

			while (resultSet.next()) {
				count = resultSet.getInt("count");
			}

			statement.close();

			/* Bale out if the user is already subscribed to this board */

			if (count > 0) {
				return;
			}

			statement = connection.prepareStatement(Queries.SUBSCRIBE_USER_TO_BOARD);
			statement.setString(1, loginId);
			statement.setInt(2, boardId);

			statement.executeUpdate();

			statement.close();

		} finally {
			Database.close(connection);
		}
	}


	public void unsubscribe(int boardId) throws SQLException {

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.UNSUBSCRIBE_USER);
			statement.setString(1, this.getLoginId());
			statement.setInt(2, boardId);
			statement.executeUpdate();

			statement.close();

		} finally {
			Database.close(connection);
		}
	}


	public List<IPAddress> getDistinctIPs(int numberOfDays) throws SQLException, IOException {

		Connection connection = null;

		List<IPAddress> distinctIPs = new LinkedList<IPAddress>();

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_DISTINCT_IPS_IN_LAST_X_DAYS);
			statement.setString(1, this.loginId);
			statement.setInt(2, numberOfDays);

			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			while (resultSet.next()) {

				String ip = resultSet.getString("ipaddress");

				if (ip.equals("0.0.0.0") || ip.equals("0:0:0:0:0:0:0:1%0")) {
					continue;
				}

				IPAddress ipAddress = new IPAddress(ip, DNS.dnsLoopup(ip));
				distinctIPs.add(ipAddress);
			}

			return distinctIPs;

		} finally {
			Database.close(connection);
		}

	}


	private boolean hasChangedUserName() {

		if (this.getOriginalDisplayName().equals(this.displayName)) {
			return false;
		} else {
			return true;
		}
	}


	private String getOriginalDisplayName() {

		String displayName = this.getDisplayName();

		if ((this.originalDisplayName != null) && (!this.originalDisplayName.trim().equals(""))) {
			displayName = this.originalDisplayName;
		}

		return displayName;
	}


	public String getRawOriginalDisplayName() {
		return this.originalDisplayName;
	}


	public String getFullDisplayName() {
		return NameTruncation.truncateName(this.getDisplayName(), this.getOriginalDisplayName());
	}


	/** Is the user able to read this board?
	 * 
	 * @param boardId
	 * @return
	 * @throws SQLException */

	public boolean canRead(int boardId) throws SQLException {

		if (boardId == 0) {
			return true;
		}

		Board board = Boards.getBoard(boardId);

		if (board.isPublicBoard() || board.isInviteOnly()) {
			return true;
		}

		if (board.isPrivateBoard()) {
			if (this.isMemberOfBoard(boardId)) {
				return true;
			}
		}

		return false;

	}


	public void yellowCard(Connection connection, String reason) throws SQLException {

		PreparedStatement statement = connection.prepareStatement(Queries.YELLOW_CARD_USER);
		statement.setString(1, reason);
		statement.setInt(2, this.getUserId());
		statement.executeUpdate();

		this.setYellowCardedOn(new Date());
		this.setYellowCardReason(reason);

	}


	/** Is this user currently yellow carded?
	 * 
	 * @return */

	public boolean isYellowCarded() {

		DateTime today = new DateTime();
		MutableDateTime yellowCarded = new MutableDateTime(this.yellowCardedOn);

		if (this.yellowCardedOn == null) {
			return false;
		}

		yellowCarded.addDays(3);

		if (yellowCarded.isAfter(today)) {
			return true;
		} else {
			return false;
		}

	}


	public void updateLastLoggedIn(String ipAddress) throws SQLException {

		if (this.getRole() == Role.Anonymous) {
			return;
		}

		DateTime today = new DateTime();
		DateTime lastLoggedIn = new DateTime(this.lastLoggedIn);

		String storedIpAddress = this.getIpAddress();

		/* If the date or the IP address is different then we need to update the database last logged in time
		 * and the local one */

		boolean newDay = ((lastLoggedIn == null) || (today.getYear() != lastLoggedIn.getYear()) || (today.getMonthOfYear() != lastLoggedIn.getMonthOfYear()) || (today.getDayOfMonth() != lastLoggedIn
				.getDayOfMonth()));
		boolean newIpAddress = ((storedIpAddress == null) || (!(storedIpAddress.equals(ipAddress))));

		if (newDay || newIpAddress) {

			/* Am I updating the details as well as the login count? If it's a different day, then update the
			 * login count too, if it's just a different IP, then don't bother. */

			String query = "";

			if (newDay) {
				query = Queries.UPDATE_USER_LAST_LOGGED_IN_AND_LOGIN_COUNT;
			} else {
				query = Queries.UPDATE_USER_LAST_LOGGED_IN;
			}

			Connection connection = null;

			try {

				connection = Database.getConnection();

				PreparedStatement statement = connection.prepareStatement(query);

				statement.setString(1, ipAddress);
				statement.setString(2, this.getLoginId());
				statement.executeUpdate();

				statement.close();

			} finally {
				Database.close(connection);
			}
		}

		if (newDay) {
			this.loginCount++;
		}

		this.lastLoggedIn = new Date();
		this.ipAddress = ipAddress;

	}


	public String getLocation() {
		return UserLocation.getLocation(this.getIpAddress());
	}


	/** Function to get all of this user's uploaded files.
	 * 
	 * @param offset
	 * @param number
	 * @return
	 * @throws SQLException */

	public List<UploadedFile> getUploadedFiles(int offset, int number) throws SQLException {
		return FileUpload.getUploadedFiles(this, offset, number);
	}


	public List<UploadedFile> getRecentlyUploadedFiles() {
		return this.recentlyUploadedFiles.getRecentlyUploadedFilesList();
	}


	/** Method to update the cached list of your recently uploaded files - only used by the
	 * FileUploadControllerServlet to keep the recentlyUploadedFiles up to date.
	 * 
	 * @param file */

	public void addUploadedFile(UploadedFile file) {
		this.recentlyUploadedFiles.add(file);
	}


	public static void main(String[] args) {

		User user = new User();
		user.setDisplayName("12345678901234567890123456789012345678901234567890123456789012345678901234567890");
		user.setOriginalDisplayName("Monkey");

		System.out.println(user.getFullDisplayName());
		System.out.println(user.getFullDisplayName().length());

		user.setDisplayName("12345678901234567890123456789012345678901234567890123456789012345678901234567890");
		user.setOriginalDisplayName("12345678901234567890123456789012345678901234567890123456789012345678901234567890");

		System.out.println(user.getFullDisplayName());
		System.out.println(user.getFullDisplayName().length());

		user.setDisplayName("XXXXXXXXXX");
		user.setOriginalDisplayName("12345678901234567890123456789012345678901234567890123456789012345678901234567890");

		System.out.println(user.getFullDisplayName());
		System.out.println(user.getFullDisplayName().length());

		user.setDisplayName("12345678901234567890123456789012345678901234567890");
		user.setOriginalDisplayName("Monkey1234567890123456789011");

		System.out.println(user.getFullDisplayName());
		System.out.println(user.getFullDisplayName().length());

	}

}
